2-1 鉴权守卫:设置鉴权用户可访问的控制器(作业)
本节深入解析 NestJS 守卫(Guard)的概念、执行机制以及自定义守卫的开发实践,为后续权限控制模块的学习奠定基础。
守卫的核心概念
守卫是一个使用 @Injectable() 装饰的类,利用 NestJS 的 DI 容器注册为实例。其职责是根据运行时条件(权限、角色、访问控制列表等)判断请求是否有权访问路由处理程序,本质上是授权机制。
守卫 vs 中间件的关键区别:
| 特性 | 守卫 | 中间件 |
|---|---|---|
| 上下文感知 | 可访问 ExecutionContext,确切知道下一步执行什么 | 不知道 next() 后执行哪个处理程序 |
| 执行位置 | 中间件之后、管道/拦截器之前 | 所有其他组件之前 |
| 适用场景 | 授权、角色判断 | 日志、请求转换 |
请求处理生命周期
客户端请求 → 中间件 → 守卫 → 管道 → 控制器/服务 → 拦截器 → 响应
text
守卫在这个生命周期中位于中间件之后、管道和拦截器之前。
自定义守卫开发
使用 CLI 创建守卫:
nest g guard common/guards/admin --flat --no-spec
bash
生成的基础结构:
// common/guards/admin.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
return true; // true = 放行,false = 403 Forbidden
}
}
typescript
守卫的两大核心能力
1. 获取请求对象
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// 可获取 request.body、request.query、request.headers 等
console.log(request.query); // { testQuery: '123' }
return true;
}
typescript
2. 获取用户信息并进行权限判断
@Injectable()
export class AdminGuard implements CanActivate {
constructor(private userService: UserService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const { user: payload } = request;
if (!payload) return false;
// 通过 payload 中的用户信息查询数据库
const user = await this.userService.find(payload.username);
// 根据用户角色判断权限
let role = user.length > 0 ? user[0].role : 1;
return role === 1;
}
}
typescript
多个守卫的执行顺序
规则一:装饰器从下往上执行
// 执行顺序:先执行 AuthGuard('jwt')(下方),再执行 AdminGuard(上方)
@UseGuards(AdminGuard)
@UseGuards(AuthGuard('jwt'))
@Get('profile')
getProfile() {}
typescript
规则二:同一 @UseGuards() 中从前往后执行
// 执行顺序:先 AuthGuard('jwt'),后 AdminGuard
// 前面的 Guard 未通过,后面的不会执行
@UseGuards(AuthGuard('jwt'), AdminGuard)
@Get('profile')
getProfile() {}
typescript
AuthGuard('jwt') 的工作原理
@nestjs/passport 提供的 AuthGuard('jwt') 在内部:
- 从请求 Header 中提取 Bearer Token
- 调用
JwtStrategy.validate()方法解析 Token - 将解析后的 payload 挂载到
request.user属性上
因此,自定义守卫若需要读取 JWT 解析后的用户信息,必须确保 AuthGuard('jwt') 先于自定义守卫执行。
模块作用域与依赖注入
在 Guard 中注入 Service 时,该 Guard 必须在使用它的模块中能找到对应的 Provider:
// AdminGuard 注入了 UserService
@Injectable()
export class AdminGuard implements CanActivate {
constructor(private userService: UserService) {}
// ...
}
// 如果 AdminGuard 在 UserController 中使用
// UserController 属于 UserModule
// UserModule 已经导入了 UserService → 不会报错
// 但如果在其他模块的 Controller 中使用 AdminGuard
// 该模块必须 imports UserModule,否则 DI 找不到 UserService
typescript
重要原则:不要在 providers 中重新声明 Service,这会创建新的实例而非复用已有实例。始终通过模块导入来共享依赖。
在控制器中使用守卫
// user.controller.ts
@Controller('user')
@UseGuards(AuthGuard('jwt'), AdminGuard)
export class UserController {
@Get('test')
test() {
return 'OK';
}
@Get('profile')
getProfile(@Request() req) {
// req.user 已由 AuthGuard('jwt') 注入
return req.user;
}
}
typescript
小结
| 知识点 | 要点 |
|---|---|
| Guard 接口 | 实现 CanActivate,返回 boolean 或 Promise<boolean> |
| 获取请求 | context.switchToHttp().getRequest() |
| 多 Guard 执行 | 装饰器从下往上,参数从前往后 |
| DI 限制 | Guard 中注入的 Service 必须在使用模块中可获取 |
| 返回值 | true 放行,false 返回 403 |
↑